package benchmark.energy.server;


import benchmark.energy.EnergyDataTxGenerator;
import benchmark.energy.tx.*;
import benchmark.server.Neo4jSocketServer;
import benchmark.transaction.definition.AbstractTransaction;
import benchmark.utils.*;
import com.alibaba.fastjson.JSON;
import com.google.common.base.Preconditions;
import org.neo4j.graphdb.*;
import org.neo4j.graphdb.index.Index;
import org.neo4j.graphdb.schema.IndexDefinition;
import org.neo4j.graphdb.schema.Schema;
import org.neo4j.index.lucene.QueryContext;

import java.io.File;
import java.io.IOException;
import java.sql.Timestamp;
import java.util.*;

public class Neo4jServer1 extends Neo4jSocketServer.ReqExecutor {

    private HashMap<Integer, Long> name2Id = new HashMap<>();

    public static void main(String[] args) {
        Neo4jSocketServer server = new Neo4jSocketServer(dbDir(), new Neo4jServer1());
        RuntimeEnv env = RuntimeEnv.getCurrentEnv();
        String serverCodeVersion = env.name() + "." + Helper.codeGitVersion();
        System.out.println("server code version:" + serverCodeVersion);
        try {
            server.start();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static File dbDir() {
        String path = Helper.mustEnv("DB_PATH");
        Preconditions.checkNotNull(path, "need arg: DB_PATH");
        File dbDir = new File(path);
        if (!dbDir.exists()) {
            if (dbDir.mkdirs()) return dbDir;
            else throw new IllegalArgumentException("invalid dbDir");
        } else if (!dbDir.isDirectory()) {
            throw new IllegalArgumentException("invalid dbDir");
        }
        return dbDir;
    }

    private enum Edge implements RelationshipType {
        NORMAL, PROP, TIME_LINE, TIME
    }

    @Override
    protected void setDB(GraphDatabaseService db) {
        this.db = db;
        try (Transaction tx = db.beginTx()) {
            Schema schema = db.schema();
            boolean hasRoadIndex = false;
            boolean hasTimeIndex = false;
            try {
                IndexDefinition roadIndex = schema.getIndexByName("ind_on_road_name");
                hasRoadIndex = true;
                System.out.println("Has the road index already.");
            } catch (IllegalArgumentException e) {
                // There is no this index.
            }
            try {
                IndexDefinition timeIndex = schema.getIndexByName("ind_on_time_st_en_time");
                hasTimeIndex = true;
                System.out.println("Has the time index already.");
            } catch (IllegalArgumentException e) {
                // There is no this index
            }
            if (!hasRoadIndex) {
                schema.indexFor(Label.label("Road")).on("start_cross_id").create();
                schema.indexFor(Label.label("Road")).on("end_cross_id").create();
                schema.indexFor(Label.label("Road")).on("name").withName("ind_on_road_name").create();
                System.out.println("Build the road index.");
            }
            if (!hasTimeIndex) {
                schema.indexFor(Label.label("Time")).on("st_time").on("en_time").withName("ind_on_time_st_en_time").create();
                System.out.println("Build the time index.");
            }
            tx.success();
        }
    }


    @Override
    protected AbstractTransaction.Result execute(String line) throws RuntimeException {
        AbstractTransaction tx = JSON.parseObject(line, AbstractTransaction.class);
        switch (tx.getTxType()) {
            case TX_IMPORT_STATIC_DATA:
                return execute((ImportStaticDataTx) tx);
            case TX_IMPORT_TEMPORAL_DATA:
                return execute((ImportTemporalDataTx) tx);
//            case TX_UPDATE_TEMPORAL_DATA:
//                return execute((UpdateTemporalDataTx) tx);
            case TX_QUERY_SNAPSHOT:
                return execute((SnapshotQueryTx) tx);
            case TX_QUERY_SNAPSHOT_AGGR_MAX:
                return execute((SnapshotAggrMaxTx) tx);
//            case TX_QUERY_SNAPSHOT_AGGR_DURATION:
//                return execute((SnapshotAggrDurationTx) tx);
            case TX_ENTITY_TEMPORAL_CONDITION_QUERY:
                return execute((EntityTemporalConditionTx) tx);
            default:
                throw new UnsupportedOperationException();
        }
    }

    private void buildMapBetweenNameAndId(GraphDatabaseService db) {
        try (Transaction tx = db.beginTx()) {
            ResourceIterator<Node> allDataNodes = db.findNodes(Label.label("NODE"));
            while (allDataNodes.hasNext()) {
                Node road = allDataNodes.next();
                name2Id.put((Integer) road.getProperty("id"), road.getId());
            }
            tx.success();
        }
    }

    private AbstractTransaction.Result execute(ImportStaticDataTx tx) {
        Label dataNode = Label.label("NODE");
        try (Transaction t = db.beginTx()) {
            for (ImportStaticDataTx.EnergyNode node : tx.getNodes()) {
                Node n = db.createNode(dataNode);
                n.setProperty("id", node.getRawId());
                n.setProperty("name", node.getName());
                n.setProperty("country", node.getCountry());
                n.setProperty("voltage", node.getVoltage());
                n.setProperty("latitude", node.getLatitude());
                n.setProperty("longitude", node.getLongitude());
                name2Id.put(node.getRawId(), n.getId());
            }
            for (ImportStaticDataTx.EnergyRel rel : tx.getRels()) {
                Node s = db.getNodeById(name2Id.get(rel.getFromRawId()));
                Node e = db.getNodeById(name2Id.get(rel.getToRawId()));
                Relationship r = s.createRelationshipTo(e, Edge.NORMAL);
                r.setProperty("x", rel.getX());
                r.setProperty("y", rel.getY());
                r.setProperty("limit", rel.getLimit());
                r.setProperty("length", rel.getLength());
            }
            t.success();
        }
        return new AbstractTransaction.Result();
    }

    private void setProperties(List<Float> values, Relationship r){
        for(int i=0; i< EnergyDataTxGenerator.properties.length; i++) {
            String propName = EnergyDataTxGenerator.properties[i];
            Float val = values.get(i);
            r.setProperty(propName, val);
        }
    }

    // Assume that data comes order by time. Thus, multithreading would not work.
    private AbstractTransaction.Result execute(ImportTemporalDataTx tx) {
        Label timeLabel = Label.label("TIME");
        try (Transaction ttx = db.beginTx()) {
            for (ImportTemporalDataTx.EnergyStatus s : tx.getData()) {
                Long id = name2Id.get(s.getNodeId());
                assert id != null;
                Node n = db.getNodeById(id);
                Node t = getProperTimeNode(s.getTimePoint(), s.getTimePoint()+3599);
                Relationship r = n.createRelationshipTo(t, Edge.TIME);
                setProperties(s.getStatus(), r);
            }
            ttx.success();
        }
        return new AbstractTransaction.Result();
    }

    private Node largestTime = null;
    private Node getProperTimeNode(int startTime, int endTime) {
        Node ret;
        try (Transaction tx = db.beginTx()) {
            ResourceIterator<Node> nodes = db.findNodes(Label.label("Time"), "st_time", startTime, "en_time", endTime);
            if (nodes.hasNext()) {
                ret = nodes.next();
            } else {
                ret = db.createNode(Label.label("Time"));
                ret.setProperty("st_time", startTime);
                ret.setProperty("en_time", endTime);
                if(largestTime!=null){
                    ret.createRelationshipTo(largestTime, Edge.TIME_LINE);
                }else{
                    largestTime = ret;
                }
            }
            tx.success();
        }
        return ret;
    }

    private void copyProperty(Relationship from, Relationship to){
        for(String propName : EnergyDataTxGenerator.properties) {
            to.setProperty(propName, from.getProperty(propName));
        }
    }

    private AbstractTransaction.Result execute(UpdateTemporalDataTx tx) {
        try (Transaction t = db.beginTx()) {
            Node node = db.findNode(Label.label("NODE"), "name", tx.getEntityId());
            // delete the old node and relationships.
            boolean hasInner = true;
            ArrayList<Relationship> willBeDeleted = new ArrayList<>();
            int mn = Integer.MAX_VALUE, mx = Integer.MIN_VALUE;
            Relationship mnRel = null, mxRel = null;
            int cnt = 0;
            for (Relationship r : node.getRelationships(Direction.BOTH, Edge.TIME)) {
                Node timeNode = r.getOtherNode(node);
                int st = (int) timeNode.getProperty("st_time");
                int en = (int) timeNode.getProperty("en_time");
                System.out.println("cur " + cnt + " , st = " + new Timestamp(st * 1000L) + ", en = " + new Timestamp(en * 1000L));
                ++cnt;
                if (tx.getEndTime() >= st && tx.getStartTime() <= en) {
                    willBeDeleted.add(r);
                    if (hasInner && tx.getStartTime() >= st && tx.getEndTime() <= en) {
                        mn = st;
                        mx = en;
                        mnRel = mxRel = r;
                        break;
                    }
                    hasInner = false;
                    if (st < mn) {
                        mnRel = r;
                        mn = st;
                    }
                    if (en > mx) {
                        mxRel = r;
                        mx = en;
                    }
                }
            }
            // update the first and last range.
            Node t1 = getProperTimeNode(mn, tx.getStartTime() - 1);
            Relationship r = t1.createRelationshipTo(node, Edge.TIME);
            copyProperty(mnRel, r);

            Node t2 = getProperTimeNode(tx.getEndTime() + 1, mx);
            r = t2.createRelationshipTo(node, Edge.TIME);
            copyProperty(mxRel, r);

            // delete the old value.
            for (Relationship d : willBeDeleted) {
                Node time = d.getStartNode();
                d.delete();
                if (time.getDegree() == 0) time.delete();
            }

            // build the new node.
            Node timeNode = getProperTimeNode(tx.getStartTime(), tx.getEndTime());
            Relationship rel = timeNode.createRelationshipTo(node, Edge.TIME);
            setProperties(tx.getValues(), rel);
            t.success();
        }
        return new AbstractTransaction.Result();
    }

    private AbstractTransaction.Result execute(SnapshotQueryTx tx) {
        try (Transaction t = db.beginTx()) {
            List<Pair<String, Float>> res = new ArrayList<>();
            int time = tx.getTimestamp();
            ResourceIterator<Node> timeNodes = db.findNodes(Label.label("Time"));
            ArrayList<Node> cache = new ArrayList<>();
            while (timeNodes.hasNext()) {
                Node node = timeNodes.next();
                int st = (int) node.getProperty("st_time");
                int en = (int) node.getProperty("en_time");
                if (time >= st && time <= en) cache.add(node);
            }
            for (Node node : cache) {
                Iterable<Relationship> rels = node.getRelationships(Direction.INCOMING, Edge.TIME);
                for (Relationship rel : rels) {
                    res.add(Pair.of((String) rel.getEndNode().getProperty("name"), (float) rel.getProperty(tx.getPropertyName())));
                }
            }
            t.success();
            SnapshotQueryTx.Result result = new SnapshotQueryTx.Result();
            result.setStatus(res);
            return result;
        }
    }

    private AbstractTransaction.Result execute(SnapshotAggrMaxTx tx) {
        try (Transaction t = db.beginTx()) {
            List<Pair<String, Float>> res = new ArrayList<>();
            int t0 = tx.getT0(), t1 = tx.getT1();
            ResourceIterator<Node> timeNodes = db.findNodes(Label.label("Time"));
            ArrayList<Node> cache = new ArrayList<>();
            HashMap<String, Float> ans = new HashMap<>();
            while (timeNodes.hasNext()) {
                Node node = timeNodes.next();
                int st = (int) node.getProperty("st_time");
                int en = (int) node.getProperty("en_time");
                if (t1 >= st && t0 <= en) {
                    cache.add(node);
                }
            }
            for (Node node : cache) {
                Iterable<Relationship> rels = node.getRelationships(Direction.INCOMING, Edge.TIME);
                for (Relationship rel : rels) {
                    ans.merge((String) rel.getEndNode().getProperty("name"), (float) rel.getProperty(tx.getP()), Float::max);
                }
            }
            for (Map.Entry<String, Float> entry : ans.entrySet()) {
                res.add(Pair.of(entry.getKey(), entry.getValue()));
            }
            t.success();
            SnapshotAggrMaxTx.Result result = new SnapshotAggrMaxTx.Result();
            result.setPropMaxValue(res);
            return result;
        }
    }

    private AbstractTransaction.Result execute(SnapshotAggrDurationTx tx) {
        TreeSet<Integer> intStarts = tx.getIntervalStarts();
        try (Transaction t = db.beginTx()) {
            List<Triple<String, Integer, Integer>> res = new ArrayList<>();
            int t0 = tx.getT0(), t1 = tx.getT1();
            ResourceIterator<Node> timeNodes = db.findNodes(Label.label("Time"));
            HashMap<String, HashMap<Integer, Integer>> ans = new HashMap<>();
            while (timeNodes.hasNext()) {
                Node node = timeNodes.next();
                int st = (int) node.getProperty("st_time");
                int en = (int) node.getProperty("en_time");
                if (t1 >= st && t0 <= en) {
                    int duration;
                    if (t0 >= st && t1 <= en) { // inner
                        duration = t1 - t0;
                    } else if (t0 >= st) { // left  t0 < st || t1 > en
                        // t1 > en => t1 >= en + 1.
                        duration = en - t0 + 1;
                    } else if (t1 >= en) { // middle t0 < st
                        if (t1 == en) duration = t1 - st;
                        else duration = en - st + 1;
                    } else { // right t0 < st && t1 < en
                        duration = t1 - st;
                    }
                    for (Relationship rel : node.getRelationships(Direction.INCOMING, Edge.TIME)) {
                        String rName = (String) rel.getOtherNode(node).getProperty("name");
                        float property = (Float) rel.getProperty(tx.getP());
                        Integer grp = intStarts.floor((int) property);
                        if(grp!=null) {
                            HashMap<Integer, Integer> tmp = ans.get(rName);
                            if (tmp != null) {
                                tmp.merge(grp, duration, Integer::sum);
                            } else {
                                tmp = new HashMap<>();
                                tmp.put(grp, duration);
                            }
                            ans.put(rName, tmp);
                        }
                    }
                }
            }
            for (Map.Entry<String, HashMap<Integer, Integer>> entry : ans.entrySet()) {
                for (Map.Entry<Integer, Integer> e : entry.getValue().entrySet()) {
                    res.add(Triple.of(entry.getKey(), e.getKey(), e.getValue()));
                }
            }
            t.success();
            SnapshotAggrDurationTx.Result result = new SnapshotAggrDurationTx.Result();
            result.setStatusDuration(res);
            return result;
        }
    }

    private AbstractTransaction.Result execute(EntityTemporalConditionTx tx) {
        try (Transaction t = db.beginTx()) {
            int t0 = tx.getT0(), t1 = tx.getT1();
            float vMin = tx.getVMin(), vMax = tx.getVMax();
            ResourceIterator<Node> timeNodes = db.findNodes(Label.label("Time"));
            HashSet<String> res = new HashSet<>();
            while (timeNodes.hasNext()) {
                Node node = timeNodes.next();
                int st = (int) node.getProperty("st_time");
                int en = (int) node.getProperty("en_time");
                if (t1 >= st && t0 <= en) {
                    Iterable<Relationship> rels = node.getRelationships(Direction.INCOMING, Edge.TIME);
                    for (Relationship rel : rels) {
                        Float property = (Float) rel.getProperty(tx.getP());
                        String rName = (String) rel.getOtherNode(node).getProperty("name");
                        if (property >= vMin && property <= vMax) {
                            res.add(rName);
                        }
                    }
                }
            }
            t.success();
            EntityTemporalConditionTx.Result result = new EntityTemporalConditionTx.Result();
            result.setEntities(new ArrayList<>(res));
            return result;
        }
    }

}
